using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;

using Pampa.CRUD.Core;
using Pampa.CRUD.Exceptions;

namespace Pampa.GenericEntity
{
    public class GenericEntity : GenericEntityBase, IIdentifiable
    {
        int id;
        string type;
        char status;
        bool flagNew;

        protected GenericEntityDBLayer dbLayer;

        public GenericEntity()
        {
            flagNew = true;
            status = 'A';
            dbLayer = new GenericEntityDBLayer();
        }

        public GenericEntity(string type) : this()
        {
            this.Type = type;
        }

        #region Properties

        public int ID
        {
            get { return id; }
            set { id = value; }
        }

        public string Type
        {
            get { return type; }
            set { type = value; }
        }

        public char Status
        {
            get { return status; }
        }

        public override bool IsNew
        {
            get { return flagNew; }
        }

        public string Id
        {
            get { return this.ID.ToString(); }
        }

        public static int ParseId(string strId)
        {
            return int.Parse(strId);
        }

        #endregion

        #region Transactions

        public override void BeginTransaction()
        {
            this.dbLayer.BeginTransaction();
        }

        public override void CommitTransaction()
        {
            this.dbLayer.CommitTransaction();
        }

        public override void RollBackTransaction()
        {
            this.dbLayer.RollBackTransaction();
        }

        #endregion

        #region Public Functions

        public override object GetPropertyValue(string propertyID)
        {
            if(propertyID == "@Id")
                return this.ID;
            else if (propertyID == "@Type")
                return this.Type;
            else
                return base.GetPropertyValue(propertyID);
        }

        public override void SetPropertyValue(string propertyID, object value)
        {
            if (propertyID == "_ID")
                this.ID = int.Parse(value.ToString());
            else if (propertyID == "_Type")
                this.Type = value.ToString();
            else
                base.SetPropertyValue(propertyID, value);
        }

        public static GenericEntity Find(object id)
        {
            GenericEntity entity = new GenericEntity();
            DataTable table = entity.dbLayer.ExecuteDataTable("select * from GenericEntity where ID = " + id.ToString());

            if (table.Rows.Count == 1)
            {
                entity.flagNew = false;
                entity.ID = (int)id;
                entity.Type = table.Rows[0]["Type"].ToString();
                entity.status = table.Rows[0]["Status"].ToString()[0];

                FindProperties(entity, id);

                entity.dbLayer.CloseConnection();
            }
            else 
            {
                entity.dbLayer.CloseConnection();

                throw (new EntityNotFoundException());
            }

            return entity;
        }

        public override void Save()
        {
            if (this.IsNew)
            {
                this.dbLayer.Execute("insert into GenericEntity (type, status) values ('" + this.Type + "', '" + this.Status + "')");
                this.ID = int.Parse(this.dbLayer.ExecuteScalar("select @@identity").ToString());
            }
            else
            {
                this.dbLayer.Execute("delete from GenericProperty where entityid = " + this.ID);
            }
            
            SaveProperties();
        }

        public override void Delete()
        {
            status = 'I';

            if(!this.IsNew)
                this.dbLayer.Execute("update GenericEntity status = 'I' where id = " + this.ID.ToString());
        }

        #endregion

        #region Private Functions

        private static void FindProperties(GenericEntity entity, object id)
        {
            DataTable dataTable = entity.dbLayer.ExecuteDataTable("select * from GenericProperty where entityID = " + id.ToString());

            foreach(DataRow row in dataTable.Rows)
            {
                char propertyType = row["Type"].ToString()[0];
                string propertyName = row["Name"].ToString();
                
                switch(propertyType)
                {
                    case 'L': // listas
                        // creo la lista
                        IList newList = new List<object>();
                        entity.SetPropertyValue(propertyName, newList);

                        // lleno la lista
                        int propertyID = (int)row["ID"];
                        FillList(entity, propertyID, newList);
                        break;

                    case 'B': // booleans
                        entity.SetPropertyValue(propertyName, bool.Parse(row["BitValue"].ToString()));
                        break;

                    case 'N': // numeric
                        entity.SetPropertyValue(propertyName, float.Parse(row["NumericValue"].ToString()));
                        break;

                    case 'D': // dateTime
                        entity.SetPropertyValue(propertyName, DateTime.Parse(row["DateTimeValue"].ToString()));
                        break;

                    case 'S': // strings
                        entity.SetPropertyValue(propertyName, row["StringValue"].ToString());
                        break;

                    case 'E': // entities
                        string entyityID = row["StringValue"].ToString();
                        string entityType = row["EntityType"].ToString();
                        
                        IActiveRecord activeRecordChild = EntityManager.LoadFromStringId(entityType, entyityID);
                        entity.SetPropertyValue(propertyName, activeRecordChild);
                        break;
                }
            }
        }

        private static void FillList(GenericEntity entity, int propertyID, IList list)
        {
            DataTable dataTable = entity.dbLayer.ExecuteDataTable("select * from GenericProperty where ParentID = " + propertyID.ToString());

            foreach (DataRow row in dataTable.Rows)
            {
                char propertyType = row["Type"].ToString()[0];
                string propertyName = row["Name"].ToString();

                if(propertyType == 'E')
                {
                    string entyityID = row["StringValue"].ToString();
                    string entityType = row["EntityType"].ToString();

                    IActiveRecord newItem = EntityManager.LoadFromStringId(entityType, entyityID);
                    list.Add(newItem);
                }
                else
                {
                    throw (new GenericEntityException("Propiedad de tipo '" + propertyType + "' no esperada."));
                }
            }
        }

        private void SaveProperties()
        {
            foreach (string propertyName in this.properties.Keys)
            {
                object property = properties[propertyName];

                if (property is IList)
                {
                    int propertyID = SaveProperty(null, 'L', null, propertyName, "StringValue", "This is a list");
                    SaveListItems((IList)property, propertyID);
                }
                else
                {
                    SaveProperty(null, propertyName, property);
                }
            }
        }

        private void SaveListItems(IList list, int propertyID)
        {
            foreach (object item in list)
            {
                if (item is IActiveRecord)
                {
                    SaveProperty(propertyID, null, item);
                }
                else
                {
                    throw (new GenericEntityException("Items in a list must implement IActiveRecord"));
                }
            }
        }

        private void SaveProperty(int? parentPropertyID, string propertyName, object property)
        {
            char propertyType;
            string propertyField;
            object propertyDBValue;

            if (property is IActiveRecord)
            {
                ((IActiveRecord)property).Save(); // Persisto a las entidades hijas por si hubo cambios (esto podria traer problemas de transacciones)

                GetPropertyDBInfo(property, out propertyType, out propertyField, out propertyDBValue);
                SaveProperty(parentPropertyID, propertyType, property.GetType().AssemblyQualifiedName, propertyName, propertyField, propertyDBValue);
            }
            else
            {
                GetPropertyDBInfo(property, out propertyType, out propertyField, out propertyDBValue);
                SaveProperty(parentPropertyID, propertyType, null, propertyName, propertyField, propertyDBValue);
            }
        }

        private int SaveProperty(int? parentID, char propertyType, string entityType, string propertyName, string propertyField, object propertyValue)
        {
            string strParentID;

            if (parentID.HasValue)
                strParentID = parentID.ToString();
            else
                strParentID = "null";

            string query = "insert into GenericProperty ";

            if(propertyField == "StringValue")
            {
                query += "(EntityID, ParentID, Type, EntityType, Name, " + propertyField + ")";
                query += " values ";
                query += "(" + this.ID.ToString() + ", " + strParentID + ", '" + propertyType + "', '" + entityType + "', '" + propertyName + "', @propertyValue)";

                this.dbLayer.Execute(query, new SqlParameter("@propertyValue", propertyValue));
            }

            // Siempre guardo una copia del string del valor en el campo StringValue
            else
            {
                query += "(EntityID, ParentID, Type, EntityType, Name, StringValue, " + propertyField + ")";
                query += " values ";
                query += "(" + this.ID.ToString() + ", " + strParentID + ", '" + propertyType + "', '" + entityType + "', '" + propertyName + "', @propertyStringValue, @propertyValue)";

                SqlParameter parameter1 = new SqlParameter("@propertyStringValue", propertyValue.ToString());
                SqlParameter parameter2 = new SqlParameter("@propertyValue", propertyValue);

                this.dbLayer.Execute(query, new SqlParameter[] { parameter1, parameter2 });
            }
 
            int propertyID = int.Parse(this.dbLayer.ExecuteScalar("select @@identity").ToString());
            return propertyID;
        }

        #endregion

        /// <summary>
        /// Este metodo determina como se van a guardar las propiedades en la base
        /// dependiendo de su tipo
        /// </summary>
        /// <param name="property">valor de la propiedad</param>
        /// <param name="propertyType">char que indica el tipo de la propiedad (esto servira para cargar correctamente las propiedades desde la base)</param>
        /// <param name="propertyField">campo en el que se va a guardar la entida, dependiendo de su tipo</param>
        /// <param name="propertyDBValue">valor que se va a guardar en la base</param>
        public static void GetPropertyDBInfo(object property, out char propertyType, out string propertyField, out object propertyDBValue)
        {
            if (property is IActiveRecord)
            {
                propertyType = 'E';
                propertyField = "StringValue";
                propertyDBValue = GetEntityID((IActiveRecord)property);
            }
            else if (property is bool)
            {
                propertyType = 'B';
                propertyField = "BitValue";
                propertyDBValue = property;
            }
            else if ((property is int) || (property is float))// podria agregar Int16, Int32, int64, double...
            {
                propertyType = 'N';
                propertyField = "NumericValue";
                propertyDBValue = property;
            }
            else if (property is DateTime)
            {
                propertyType = 'D';
                propertyField = "DateTimeValue";
                propertyDBValue = property;
            }
            else
            {
                propertyType = 'S';
                propertyField = "StringValue";
                propertyDBValue = property.ToString();
            }
        }

        private static string GetEntityID(IActiveRecord activeRecord)
        {
            if (activeRecord is IIdentifiable)
            {
                return ((IIdentifiable)activeRecord).Id;
            }
            else if (activeRecord is IIdDescription)
            {
                return ((IIdDescription)activeRecord).Id;
            }
            else
            {
                throw (new GenericEntityException("Class " + activeRecord.GetType().Name + " should implement IIdentifiable or IIdDescription."));
            }
        }
    }
}
